Passed
Branch master (0da6e1)
by Dave
54s
created

eventsource.js ➔ ... ➔ ?!?.request   C

Complexity

Conditions 11
Paths 8

Size

Total Lines 97
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 11
eloc 59
c 1
b 0
f 1
nc 8
dl 0
loc 97
rs 5.1218
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like eventsource.js ➔ ... ➔ ?!?.request often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
var original = require('original')
2
var parse = require('url').parse
3
var events = require('events')
4
var https = require('https')
5
var http = require('http')
6
var util = require('util')
7
8
var httpsOptions = [
9
  'pfx', 'key', 'passphrase', 'cert', 'ca', 'ciphers',
10
  'rejectUnauthorized', 'secureProtocol', 'servername'
11
]
12
13
/**
14
 * Creates a new EventSource object
15
 *
16
 * @param {String} url the URL to which to connect
17
 * @param {Object} [eventSourceInitDict] extra init params. See README for details.
18
 * @api public
19
 **/
20
function EventSource (url, eventSourceInitDict) {
21
  var readyState = EventSource.CONNECTING
22
  Object.defineProperty(this, 'readyState', {
23
    get: function () {
24
      return readyState
25
    }
26
  })
27
28
  Object.defineProperty(this, 'url', {
29
    get: function () {
30
      return url
31
    }
32
  })
33
34
  var self = this
35
  self.reconnectInterval = 60000
36
37
  function onConnectionClosed () {
38
    if (readyState === EventSource.CLOSED) return
39
    readyState = EventSource.CONNECTING
40
    _emit('error', new Event('error'))
41
42
    // The url may have been changed by a temporary
43
    // redirect. If that's the case, revert it now.
44
    if (reconnectUrl) {
45
      url = reconnectUrl
46
      reconnectUrl = null
47
    }
48
    setTimeout(function () {
49
      if (readyState !== EventSource.CONNECTING) {
50
        return
51
      }
52
      connect()
53
    }, self.reconnectInterval)
54
  }
55
56
  var req
57
  var lastEventId = ''
58
  if (eventSourceInitDict && eventSourceInitDict.headers && eventSourceInitDict.headers['Last-Event-ID']) {
59
    lastEventId = eventSourceInitDict.headers['Last-Event-ID']
60
    delete eventSourceInitDict.headers['Last-Event-ID']
61
  }
62
63
  var discardTrailingNewline = false
64
  var data = ''
65
  var eventName = ''
66
67
  var reconnectUrl = null
68
69
  function connect () {
70
    var options = parse(url)
71
    var isSecure = options.protocol === 'https:'
72
    options.headers = { 'Cache-Control': 'no-cache', 'Accept': 'text/event-stream' }
73
    if (lastEventId) options.headers['Last-Event-ID'] = lastEventId
74
    if (eventSourceInitDict && eventSourceInitDict.headers) {
75
      for (var i in eventSourceInitDict.headers) {
76
        var header = eventSourceInitDict.headers[i]
77
        if (header) {
78
          options.headers[i] = header
79
        }
80
      }
81
    }
82
83
    // Legacy: this should be specified as `eventSourceInitDict.https.rejectUnauthorized`,
84
    // but for now exists as a backwards-compatibility layer
85
    options.rejectUnauthorized = !(eventSourceInitDict && !eventSourceInitDict.rejectUnauthorized)
86
87
    // If specify http proxy, make the request to sent to the proxy server,
88
    // and include the original url in path and Host headers
89
    var useProxy = eventSourceInitDict && eventSourceInitDict.proxy
90
    if (useProxy) {
91
      var proxy = parse(eventSourceInitDict.proxy)
92
      isSecure = proxy.protocol === 'https:'
93
94
      options.protocol = isSecure ? 'https:' : 'http:'
95
      options.path = url
96
      options.headers.Host = options.host
97
      options.hostname = proxy.hostname
98
      options.host = proxy.host
99
      options.port = proxy.port
100
    }
101
102
    // If https options are specified, merge them into the request options
103
    if (eventSourceInitDict && eventSourceInitDict.https) {
104
      for (var optName in eventSourceInitDict.https) {
105
        if (httpsOptions.indexOf(optName) === -1) {
106
          continue
107
        }
108
109
        var option = eventSourceInitDict.https[optName]
110
        if (option !== undefined) {
111
          options[optName] = option
112
        }
113
      }
114
    }
115
116
    // Pass this on to the XHR
117
    if (eventSourceInitDict && eventSourceInitDict.withCredentials !== undefined) {
118
      options.withCredentials = eventSourceInitDict.withCredentials
119
    }
120
121
    req = (isSecure ? https : http).request(options, function (res) {
122
      // Handle HTTP errors
123
      if (res.statusCode === 500 || res.statusCode === 502 || res.statusCode === 503 || res.statusCode === 504) {
124
        _emit('error', new Event('error', {status: res.statusCode}))
125
        onConnectionClosed()
126
        return
127
      }
128
129
      // Handle HTTP redirects
130
      if (res.statusCode === 301 || res.statusCode === 307) {
131
        if (!res.headers.location) {
132
          // Server sent redirect response without Location header.
133
          _emit('error', new Event('error', {status: res.statusCode}))
134
          return
135
        }
136
        if (res.statusCode === 307) reconnectUrl = url
137
        url = res.headers.location
138
        process.nextTick(connect)
139
        return
140
      }
141
142
      if (res.statusCode !== 200) {
143
        _emit('error', new Event('error', {status: res.statusCode}))
144
        return self.close()
145
      }
146
147
      // protect against multiple connects
148
      //https://github.com/tigertext/eventsource/commit/ca8a6e0ca0db10c23ba7bf2b7f8affaa23d7a265
149
      if (readyState === EventSource.OPEN) {
150
          return;
151
      }
152
153
      readyState = EventSource.OPEN
154
      res.on('close', function () {
155
        res.removeAllListeners('close')
156
        res.removeAllListeners('end')
157
        onConnectionClosed()
158
      })
159
160
      res.on('end', function () {
161
        res.removeAllListeners('close')
162
        res.removeAllListeners('end')
163
        onConnectionClosed()
164
      })
165
      _emit('open', new Event('open'))
166
167
      // text/event-stream parser adapted from webkit's
168
      // Source/WebCore/page/EventSource.cpp
169
      var buf = ''
170
      res.on('data', function (chunk) {
171
        buf += chunk
172
173
        var pos = 0
174
        var length = buf.length
175
176
        while (pos < length) {
177
          if (discardTrailingNewline) {
178
            if (buf[pos] === '\n') {
179
              ++pos
180
            }
181
            discardTrailingNewline = false
182
          }
183
184
          var lineLength = -1
185
          var fieldLength = -1
186
          var c
187
188
          for (var i = pos; lineLength < 0 && i < length; ++i) {
189
            c = buf[i]
190
            if (c === ':') {
191
              if (fieldLength < 0) {
192
                fieldLength = i - pos
193
              }
194
            } else if (c === '\r') {
195
              discardTrailingNewline = true
196
              lineLength = i - pos
197
            } else if (c === '\n') {
198
              lineLength = i - pos
199
            }
200
          }
201
202
          if (lineLength < 0) {
203
            break
204
          }
205
206
          parseEventStreamLine(buf, pos, fieldLength, lineLength)
207
208
          pos += lineLength + 1
209
        }
210
211
        if (pos === length) {
212
          buf = ''
213
        } else if (pos > 0) {
214
          buf = buf.slice(pos)
215
        }
216
      })
217
    })
218
219
    req.on('error', onConnectionClosed)
220
    if (req.setNoDelay) req.setNoDelay(true)
221
    req.end()
222
  }
223
224
  connect()
225
226
  function _emit () {
227
    if (self.listeners(arguments[0]).length > 0) {
228
      self.emit.apply(self, arguments)
229
    }
230
  }
231
232
  this._close = function () {
233
    if (readyState === EventSource.CLOSED) return
234
    readyState = EventSource.CLOSED
235
    if (req.abort) req.abort()
236
    if (req.xhr && req.xhr.abort) req.xhr.abort()
237
  }
238
239
  function parseEventStreamLine (buf, pos, fieldLength, lineLength) {
240
    if (lineLength === 0) {
241
      if (data.length > 0) {
242
        var type = eventName || 'message'
243
        _emit(type, new MessageEvent(type, {
244
          data: data.slice(0, -1), // remove trailing newline
245
          lastEventId: lastEventId,
246
          origin: original(url)
247
        }))
248
        data = ''
249
      }
250
      eventName = void 0
251
    } else if (fieldLength > 0) {
252
      var noValue = fieldLength < 0
253
      var step = 0
254
      var field = buf.slice(pos, pos + (noValue ? lineLength : fieldLength))
255
256
      if (noValue) {
257
        step = lineLength
258
      } else if (buf[pos + fieldLength + 1] !== ' ') {
259
        step = fieldLength + 1
260
      } else {
261
        step = fieldLength + 2
262
      }
263
      pos += step
264
265
      var valueLength = lineLength - step
266
      var value = buf.slice(pos, pos + valueLength)
267
268
      if (field === 'data') {
269
        data += value + '\n'
270
      } else if (field === 'event') {
271
        eventName = value
272
      } else if (field === 'id') {
273
        lastEventId = value
274
      } else if (field === 'retry') {
275
        var retry = parseInt(value, 10)
276
        if (!Number.isNaN(retry)) {
277
          self.reconnectInterval = retry
278
        }
279
      }
280
    }
281
  }
282
}
283
284
module.exports = EventSource
285
286
util.inherits(EventSource, events.EventEmitter)
287
EventSource.prototype.constructor = EventSource; // make stacktraces readable
288
289
['open', 'error', 'message'].forEach(function (method) {
290
  Object.defineProperty(EventSource.prototype, 'on' + method, {
291
    /**
292
     * Returns the current listener
293
     *
294
     * @return {Mixed} the set function or undefined
295
     * @api private
296
     */
297
    get: function get () {
298
      var listener = this.listeners(method)[0]
299
      return listener ? (listener._listener ? listener._listener : listener) : undefined
300
    },
301
302
    /**
303
     * Start listening for events
304
     *
305
     * @param {Function} listener the listener
306
     * @return {Mixed} the set function or undefined
307
     * @api private
308
     */
309
    set: function set (listener) {
310
      this.removeAllListeners(method)
311
      this.addEventListener(method, listener)
312
    }
313
  })
314
})
315
316
/**
317
 * Ready states
318
 */
319
Object.defineProperty(EventSource, 'CONNECTING', {enumerable: true, value: 0})
320
Object.defineProperty(EventSource, 'OPEN', {enumerable: true, value: 1})
321
Object.defineProperty(EventSource, 'CLOSED', {enumerable: true, value: 2})
322
323
EventSource.prototype.CONNECTING = 0
324
EventSource.prototype.OPEN = 1
325
EventSource.prototype.CLOSED = 2
326
327
/**
328
 * Closes the connection, if one is made, and sets the readyState attribute to 2 (closed)
329
 *
330
 * @see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close
331
 * @api public
332
 */
333
EventSource.prototype.close = function () {
334
  this._close()
335
}
336
337
/**
338
 * Emulates the W3C Browser based WebSocket interface using addEventListener.
339
 *
340
 * @param {String} type A string representing the event type to listen out for
341
 * @param {Function} listener callback
342
 * @see https://developer.mozilla.org/en/DOM/element.addEventListener
343
 * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
344
 * @api public
345
 */
346
EventSource.prototype.addEventListener = function addEventListener (type, listener) {
347
  if (typeof listener === 'function') {
348
    // store a reference so we can return the original function again
349
    listener._listener = listener
350
    this.on(type, listener)
351
  }
352
}
353
354
/**
355
 * Emulates the W3C Browser based WebSocket interface using removeEventListener.
356
 *
357
 * @param {String} type A string representing the event type to remove
358
 * @param {Function} listener callback
359
 * @see https://developer.mozilla.org/en/DOM/element.removeEventListener
360
 * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
361
 * @api public
362
 */
363
EventSource.prototype.removeEventListener = function removeEventListener (type, listener) {
364
  if (typeof listener === 'function') {
365
    listener._listener = undefined
366
    this.removeListener(type, listener)
367
  }
368
}
369
370
/**
371
 * W3C Event
372
 *
373
 * @see http://www.w3.org/TR/DOM-Level-3-Events/#interface-Event
374
 * @api private
375
 */
376
function Event (type, optionalProperties) {
377
  Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true })
378
  if (optionalProperties) {
379
    for (var f in optionalProperties) {
380
      if (optionalProperties.hasOwnProperty(f)) {
381
        Object.defineProperty(this, f, { writable: false, value: optionalProperties[f], enumerable: true })
382
      }
383
    }
384
  }
385
}
386
387
/**
388
 * W3C MessageEvent
389
 *
390
 * @see http://www.w3.org/TR/webmessaging/#event-definitions
391
 * @api private
392
 */
393
function MessageEvent (type, eventInitDict) {
394
  Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true })
395
  for (var f in eventInitDict) {
396
    if (eventInitDict.hasOwnProperty(f)) {
397
      Object.defineProperty(this, f, { writable: false, value: eventInitDict[f], enumerable: true })
398
    }
399
  }
400
}
401